// Copyright (c) 2018, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:collection' show SplayTreeMap;
import 'dart:convert' as json;
import 'dart:io';

import 'package:args/args.dart';
import 'package:compiler/src/common.dart';
import 'package:compiler/src/compiler.dart';
import 'package:compiler/src/ir/util.dart';
import 'package:compiler/src/util/memory_compiler.dart';
import 'package:compiler/src/phase/load_kernel.dart' as load_kernel;
import 'package:expect/async_helper.dart';
import 'package:expect/expect.dart';
import 'package:front_end/src/api_unstable/dart2js.dart' show relativizeUri;
import 'package:kernel/ast.dart' as ir;
import 'package:kernel/class_hierarchy.dart' as ir;
import 'package:kernel/core_types.dart' as ir;
import 'package:kernel/type_environment.dart' as ir;

import '../helpers/args_helper.dart';

main(List<String> args) {
  ArgParser argParser = createArgParser();
  ArgResults argResults = argParser.parse(args);

  Uri? entryPoint = getEntryPoint(argResults);
  if (entryPoint == null) {
    throw ArgumentError("Missing entry point.");
  }
  Uri? librariesSpecificationUri = getLibrariesSpec(argResults);
  Uri? packageConfig = getPackages(argResults);
  List<String> options = getOptions(argResults);
  run(
    entryPoint,
    null,
    analyzedUrisFilter: (Uri uri) => !uri.isScheme('dart'),
    librariesSpecificationUri: librariesSpecificationUri,
    packageConfig: packageConfig,
    options: options,
  );
}

run(
  Uri entryPoint,
  String? allowedListPath, {
  Map<String, String> memorySourceFiles = const {},
  Uri? librariesSpecificationUri,
  Uri? packageConfig,
  bool verbose = false,
  bool generate = false,
  List<String> options = const <String>[],
  bool analyzedUrisFilter(Uri uri)?,
}) {
  asyncTest(() async {
    Compiler compiler = await compilerFor(
      memorySourceFiles: memorySourceFiles,
      librariesSpecificationUri: librariesSpecificationUri,
      packageConfig: packageConfig,
      entryPoint: entryPoint,
      options: options,
    );
    load_kernel.Output result =
        (await load_kernel.run(
          load_kernel.Input(
            compiler.options,
            compiler.provider,
            compiler.reporter,
            compiler.initializedCompilerState,
            false,
          ),
        ))!;
    compiler.frontendStrategy.registerLoadedLibraries(
      result.component,
      result.libraries!,
    );
    final coreTypes = ir.CoreTypes(result.component);
    final classHierarchy = ir.ClassHierarchy(result.component, coreTypes);
    final typeEnvironment = ir.TypeEnvironment(coreTypes, classHierarchy);
    DynamicVisitor(
      compiler.reporter,
      result.component,
      allowedListPath,
      analyzedUrisFilter,
      coreTypes,
      typeEnvironment,
    ).run(verbose: verbose, generate: generate);
  });
}

class DynamicVisitor extends ir.RecursiveVisitor {
  final DiagnosticReporter reporter;
  final ir.Component component;
  final String? _allowedListPath;
  final bool Function(Uri uri)? analyzedUrisFilter;
  late ir.StaticTypeContext staticTypeContext;
  final ir.TypeEnvironment typeEnvironment;
  final ir.CoreTypes coreTypes;

  Map _expectedJson = {};
  final Map<String, Map<String, List<DiagnosticMessage>>> _actualMessages = {};

  DynamicVisitor(
    this.reporter,
    this.component,
    this._allowedListPath,
    this.analyzedUrisFilter,
    this.coreTypes,
    this.typeEnvironment,
  );

  void run({bool verbose = false, bool generate = false}) {
    if (!generate && _allowedListPath != null) {
      File file = File(_allowedListPath);
      if (file.existsSync()) {
        try {
          _expectedJson = json.jsonDecode(file.readAsStringSync());
        } catch (e) {
          Expect.fail('Error reading allowed list from $_allowedListPath: $e');
        }
      }
    }
    component.accept(this);
    if (generate && _allowedListPath != null) {
      Map<String, Map<String, int>> actualJson = SplayTreeMap();
      _actualMessages.forEach((
        String uri,
        Map<String, List<DiagnosticMessage>> actualMessagesMap,
      ) {
        Map<String, int> map = SplayTreeMap();
        actualMessagesMap.forEach((
          String message,
          List<DiagnosticMessage> actualMessages,
        ) {
          map[message] = actualMessages.length;
        });
        actualJson[uri] = map;
      });

      File(_allowedListPath).writeAsStringSync(
        json.JsonEncoder.withIndent('  ').convert(actualJson),
      );
      return;
    }

    int errorCount = 0;
    _expectedJson.forEach((uri, expectedMessages) {
      Map<String, List<DiagnosticMessage>>? actualMessagesMap =
          _actualMessages[uri];
      if (actualMessagesMap == null) {
        print(
          "Error: Allowed-listing of uri '$uri' isn't used. "
          "Remove it from the allowed-list.",
        );
        errorCount++;
      } else {
        expectedMessages.forEach((expectedMessage, expectedCount) {
          List<DiagnosticMessage>? actualMessages =
              actualMessagesMap[expectedMessage];
          if (actualMessages == null) {
            print(
              "Error: Allowed-listing of message '$expectedMessage' "
              "in uri '$uri' isn't used. Remove it from the allowed-list.",
            );
            errorCount++;
          } else {
            int actualCount = actualMessages.length;
            if (actualCount != expectedCount) {
              print(
                "Error: Unexpected count of allowed message "
                "'$expectedMessage' in uri '$uri'. "
                "Expected $expectedCount, actual $actualCount:",
              );
              print(
                '----------------------------------------------------------',
              );
              for (DiagnosticMessage message in actualMessages) {
                reporter.reportError(message);
              }
              print(
                '----------------------------------------------------------',
              );
              errorCount++;
            }
          }
        });
        actualMessagesMap.forEach((
          String message,
          List<DiagnosticMessage> actualMessages,
        ) {
          if (!expectedMessages.containsKey(message)) {
            for (DiagnosticMessage message in actualMessages) {
              reporter.reportError(message);
              errorCount++;
            }
          }
        });
      }
    });
    _actualMessages.forEach((
      String uri,
      Map<String, List<DiagnosticMessage>> actualMessagesMap,
    ) {
      if (!_expectedJson.containsKey(uri)) {
        actualMessagesMap.forEach((
          String message,
          List<DiagnosticMessage> actualMessages,
        ) {
          for (DiagnosticMessage message in actualMessages) {
            reporter.reportError(message);
            errorCount++;
          }
        });
      }
    });
    if (errorCount != 0) {
      print('$errorCount error(s) found.');
      print("""

********************************************************************************
*  Unexpected dynamic invocations found by test:
*
*    ${relativizeUri(Uri.base, Platform.script, Platform.isWindows)}
*
*  Please address the reported errors, or, if the errors are as expected, run
*
*    dart ${relativizeUri(Uri.base, Platform.script, Platform.isWindows)} -g
*
*  to update the expectation file.
********************************************************************************
""");
      exit(-1);
    }
    if (verbose) {
      _actualMessages.forEach((
        String uri,
        Map<String, List<DiagnosticMessage>> actualMessagesMap,
      ) {
        actualMessagesMap.forEach((
          String message,
          List<DiagnosticMessage> actualMessages,
        ) {
          for (DiagnosticMessage message in actualMessages) {
            reporter.reportErrorMessage(
              message.sourceSpan,
              MessageKind.generic,
              {'text': '${message.message} (allowed)'},
            );
          }
        });
      });
    } else {
      int total = 0;
      _actualMessages.forEach((
        String uri,
        Map<String, List<DiagnosticMessage>> actualMessagesMap,
      ) {
        int count = 0;
        actualMessagesMap.forEach((
          String message,
          List<DiagnosticMessage> actualMessages,
        ) {
          count += actualMessages.length;
        });

        print('${count} error(s) allowed in $uri');
        total += count;
      });
      if (total > 0) {
        print('${total} error(s) allowed in total.');
      }
    }
  }

  @override
  void visitLibrary(ir.Library node) {
    if (analyzedUrisFilter != null) {
      if (analyzedUrisFilter!(node.importUri)) {
        return node.visitChildren(this);
      }
    } else {
      return node.visitChildren(this);
    }
  }

  @override
  void visitProcedure(ir.Procedure node) {
    if (node.kind == ir.ProcedureKind.Factory && node.isRedirectingFactory) {
      // Don't visit redirecting factories.
      return;
    }
    super.visitProcedure(node);
  }

  @override
  void defaultMember(ir.Member node) {
    staticTypeContext = ir.StaticTypeContext(node, typeEnvironment);
    super.defaultMember(node);
  }

  @override
  void visitDynamicGet(ir.DynamicGet node) {
    if (node.receiver.getStaticType(staticTypeContext) is ir.DynamicType) {
      registerError(node, "Dynamic access of '${node.name}'.");
    }
  }

  @override
  void visitDynamicSet(ir.DynamicSet node) {
    if (node.receiver.getStaticType(staticTypeContext) is ir.DynamicType) {
      registerError(node, "Dynamic update to '${node.name}'.");
    }
  }

  @override
  void visitDynamicInvocation(ir.DynamicInvocation node) {
    if (node.receiver.getStaticType(staticTypeContext) is ir.DynamicType) {
      registerError(node, "Dynamic invocation of '${node.name}'.");
    }
  }

  String reportAssertionFailure(ir.TreeNode node, String message) {
    SourceSpan span = computeSourceSpanFromTreeNode(node);
    Uri uri = span.uri;
    if (uri.isScheme('org-dartlang-sdk')) {
      span = SourceSpan(
        Uri.base.resolve(uri.path.substring(1)),
        span.begin,
        span.end,
      );
    }
    DiagnosticMessage diagnosticMessage = reporter.createMessage(
      span,
      MessageKind.generic,
      {'text': message},
    );
    reporter.reportError(diagnosticMessage);
    return message;
  }

  void registerError(ir.TreeNode node, String message) {
    SourceSpan span = computeSourceSpanFromTreeNode(node);
    Uri uri = span.uri;
    String uriString = relativizeUri(Uri.base, uri, Platform.isWindows);
    Map<String, List<DiagnosticMessage>> actualMap = _actualMessages
        .putIfAbsent(uriString, () => <String, List<DiagnosticMessage>>{});
    if (uri.isScheme('org-dartlang-sdk')) {
      span = SourceSpan(
        Uri.base.resolve(uri.path.substring(1)),
        span.begin,
        span.end,
      );
    }
    DiagnosticMessage diagnosticMessage = reporter.createMessage(
      span,
      MessageKind.generic,
      {'text': message},
    );
    actualMap
        .putIfAbsent(message, () => <DiagnosticMessage>[])
        .add(diagnosticMessage);
  }
}
